using System;
using System.IO;
using System.Text;
using System.Reflection;
using System.Collections.Generic;

using Crainiate.Data.Persistence.Cache;

namespace Crainiate.Data.Persistence.Providers
{
    public abstract class Deployment
    {
        //Property variables
        private Context _context;
        private DeploymentMode _deploymentMode;
        private DeploymentType _deploymentType;
        
        #region Interface

        //Events
        public event DeployAssemblyHandler PreDeployAssembly;
        public event DeployAssemblyHandler PostDeployAssembly;

        public event PreValidateTypeEventHandler PreValidateType;
        public event PostValidateTypeEventHandler PostValidateType;
        public event DeployTypeExceptionHandler DeployTypeException;

        public event PreDeployTypeHandler PreDeployType;
        public event PostDeployTypeHandler PostDeployType;

        public event AddTableEventHandler AddTable;
        public event ChangeTableEventHandler ChangeTable;
        public event ReferenceEventHandler AddReference;

        //Constructors
        public Deployment()
        {
            DeploymentMode = DeploymentMode.Update;
            DeploymentType = DeploymentType.Validate; 
        }

        public Deployment(Context context)
        {
            DeploymentMode = DeploymentMode.Update;
            DeploymentType = DeploymentType.Validate;

            Context = context;
        }

        //Properties
        public virtual Context Context
        {
            get
            {
                return _context;
            }
            set
            {
                _context = value;
            }
        }

        public virtual DeploymentMode DeploymentMode
        {
            get
            {
                return _deploymentMode;
            }
            set
            {
                _deploymentMode = value;
            }
        }

        public virtual DeploymentType DeploymentType
        {
            get
            {
                return _deploymentType;
            }
            set
            {
                _deploymentType = value;
            }
        }

        //Methods
        public abstract void CreateDatabase(string name);
        public abstract string[] GetDatabaseNames();
        public abstract void Execute(StringBuilder builder);
        public abstract string GetTableName(Type type);
        public abstract string SqlFromType(Type type);
        public abstract string DefaultFromType(Type type);
        public abstract bool Exists(Type type);
        public abstract bool HasRows(Type type);
        public abstract bool HasChanged(Type type, TableDefinition tableDefinition, KeyDefinition keyDefinition);

        public abstract TableDefinition GetTableDefinition(Type type);
        public abstract KeyDefinition GetKeyDefinition(Type type);

        public abstract void CreateTable(StringBuilder builder, Type type, string name);
        public abstract void AlterTable(StringBuilder builder, Type type, TableDefinition definition, TableModification modification);
        public abstract void ValidateKey(StringBuilder builder, Type type, KeyDefinition keyDefinition);
        public abstract void ValidateReference(StringBuilder builder, Type type, PropertyInfo[] properties, Type reftype);
        public abstract void RemoveReferences(StringBuilder builder, Type type);
        public abstract void RemoveProcedures(StringBuilder builder, Type type);

        //Determines if a type is nullable
        public static bool GetTypeNullability(Type type)
        {
            if (typeof(IPersistable).IsAssignableFrom(type))
            {
                return true;
            }
            else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                return true;
            }
            else if (type == typeof(string))
            {
                return true;
            }
            else if (typeof(Stream).IsAssignableFrom(type))
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        public static bool GetTypeConcurrency(Type type)
        {
            if (typeof(Stream).IsAssignableFrom(type))
            {
                return false;
            }
            else
            {
                return true;
            }
        }

        public static bool IsDeployable(Type type)
        {
            bool ipers = typeof(IPersistable).IsAssignableFrom(type);
            bool view = typeof(IView).IsAssignableFrom(type);

            return (type.IsClass && typeof(IPersistable).IsAssignableFrom(type) && !typeof(IView).IsAssignableFrom(type) && !type.IsAbstract);
        }

        //Deploys an assembly to the context provided, making any changes necessary.
        public virtual void Deploy(Assembly assembly)
        {
            if (Context == null) throw new DeploymentException("Context has not been set. Set the Context property to provide a context to deploy to.");

            DeployAssemblyEventArgs e = new DeployAssemblyEventArgs(assembly, DeploymentMode, DeploymentType);
            OnPreDeployAssembly(this, e);

            if (!e.Cancel)
            {
                //Loop through and get all IPersistable, check if they have been created in this database
                Type[] types = assembly.GetTypes();

                //Deploy each type and its primary key contraints
                foreach (Type type in types)
                {
                    if (IsDeployable(type))
                    {
                        PreValidateTypeEventArgs e2 = new PreValidateTypeEventArgs(type);
                        OnPreValidateType(this, e2);

                        if (!e2.Cancel)
                        {
                            TableDefinition tableDefinition = null;
                            KeyDefinition keyDefinition = null;

                            bool exists = Exists(type);
                            bool changed = false;

                            //See if needs to be modified
                            if (exists)
                            {
                                tableDefinition = GetTableDefinition(type);
                                keyDefinition = GetKeyDefinition(type);
                                changed = HasChanged(type, tableDefinition, keyDefinition);
                            }

                            PostValidateTypeEventArgs e3 = new PostValidateTypeEventArgs(type);
                            e3.Changed = changed;
                            e3.Exists = exists;
                            OnPostValidateType(this, e3);

                            //Deploys a type with exception and event handling
                            if (!exists || changed) DeployType(type, tableDefinition, keyDefinition, exists);
                        }
                    }
                }

                //Validate the primary keys
                foreach (Type type in types)
                {
                    if (IsDeployable(type))
                    {
                        if (Exists(type))
                        {
                            KeyDefinition keyDefinition = GetKeyDefinition(type);
                            DeployPrimaryKey(type, keyDefinition);
                        }
                    }
                }

                //Validate the foreign key references. All tables need to be built first.
                foreach (Type type in types)
                {
                    if (IsDeployable(type))
                    {
                        DeployReferences(type);
                    }
                }

                OnPostDeployAssembly(this, e);
            }
        }

        protected virtual void OnPreDeployAssembly(object sender, DeployAssemblyEventArgs e)
        {
            if (PreDeployAssembly != null) PreDeployAssembly(sender, e);
        }
        protected virtual void OnPostDeployAssembly(object sender, DeployAssemblyEventArgs e)
        {
            if (PostDeployAssembly != null) PostDeployAssembly(sender, e);
        }
        protected virtual void OnPreValidateType(object sender, PreValidateTypeEventArgs e)
        {
            if (PreValidateType != null) PreValidateType(sender ,e);
        }
        protected virtual void OnPostValidateType(object sender, PostValidateTypeEventArgs e)
        {
            if (PostValidateType != null) PostValidateType(sender, e);
        }
        protected virtual void OnPreDeployType(object sender, PreDeployTypeEventArgs e)
        {
            if (PreDeployType != null) PreDeployType(sender, e);
        }
        protected virtual void OnPostDeployType(object sender, PostDeployTypeEventArgs e)
        {
            if (PostDeployType != null) PostDeployType(sender, e);
        }
        protected virtual void OnDeployTypeException(object sender, DeployTypeExceptionEventArgs e)
        {
            if (DeployTypeException != null) DeployTypeException(sender, e);
        }
        protected virtual void OnAddTable(object sender, AddTableEventInfo e)
        {
            if (AddTable != null) AddTable(sender, e);
        }
        protected virtual void OnChangeTable(object sender, ChangeTableEventInfo e)
        {
            if (ChangeTable != null) ChangeTable(sender, e);
        }

        protected virtual void OnAddReference(object sender, ReferenceEventInfo e)
        {
            if (AddReference != null) AddReference(sender, e);
        }

        #endregion

        #region Implementation

        private void DeployType(Type type, TableDefinition tableDefinition, KeyDefinition keyDefinition, bool exists)
        {
            //Raise event, cancel if user cancels deployment
            string name = GetTableName(type);
            PreDeployTypeEventArgs e = new PreDeployTypeEventArgs(type, name);

            OnPreDeployType(this, e);

            if (e.Cancel) return;

            StringBuilder builder = new StringBuilder();

            //Alter or create table
            if (exists)
            {
                TableModification tableModification = GetTableModification(type, tableDefinition);
                OnChangeTable(this, new ChangeTableEventInfo(type, name, tableDefinition, keyDefinition, tableModification));

                RemoveReferences(builder, type);
                RemoveProcedures(builder, type);
                AlterTable(builder, type, tableDefinition, tableModification );
            }
            else
            {
                OnAddTable(this, new AddTableEventInfo(type, name, tableDefinition));
                CreateTable(builder, type, name);
            }

            //Deploy if required
            if (DeploymentType == DeploymentType.Deploy)
            {
                try
                {
                    Execute(builder);
                }
                catch (Exception ex)
                {
                    DeployTypeExceptionEventArgs args = new DeployTypeExceptionEventArgs(type, name, builder.ToString(), ex);
                    OnDeployTypeException(this, args);

                    //Setting the exception to null in the event stops the exception from being raised
                    if (args.Exception != null) throw new TypeDeploymentException("An error occurred deploying type " + type.FullName + ". " + ex.Message, ex);
                    
                    return;
                }
            }

            //Only create event object if events are added to the delegate
            PostDeployTypeEventArgs e2 = new PostDeployTypeEventArgs(type, name, builder.ToString());
            OnPostDeployType(this, e2);
        }

        private void DeployPrimaryKey(Type type, KeyDefinition keyDefinition)
        {
            ReflectionCacheItem cache = Component.Instance.ReflectionCache.Lookup(type);
            StringBuilder builder = new StringBuilder();

            ValidateKey(builder, type, keyDefinition);

            //Deploy if required
            if (DeploymentType == DeploymentType.Deploy) Execute(builder);
        }

        private void DeployReferences(Type type)
        {
            ReflectionCacheItem cache = Component.Instance.ReflectionCache.Lookup(type);
            StringBuilder builder = new StringBuilder();

            //Validate that the keys in an inherited type reference the base type
            if (!cache.IsUnderlying) ValidateReference(builder, type, cache.GetKeyProperties(), cache.BaseType);

            //Validate a foreign key to another table
            if (cache.HasForeignKeys)
            {
                foreach (PropertyInfo prop in cache.ForeignKeys)
                {
                    ForeignKeyAttribute attr = cache.GetForeignKeyAttribute(prop);
                    if (attr != null) ValidateReference(builder, type, new PropertyInfo[] { prop }, attr.References);
                }
            }

            //Check property references
            //Loop through each property          
            foreach (PropertyInfo prop in cache.Properties)
            {
                if (typeof(IPersistable).IsAssignableFrom(prop.PropertyType))
                {
                    ValidateReference(builder, type, new PropertyInfo[] { prop }, prop.PropertyType);
                }
            }

            //Deploy if required
            if (DeploymentType == DeploymentType.Deploy) Execute(builder);
        }

        private TableModification GetTableModification(Type type, TableDefinition definition)
        {
            //Get the properties from the type
            TableModification modification = new TableModification();
            ReflectionCacheItem cache = Component.Instance.ReflectionCache.Lookup(type);
            string propname;
            int index;

            //Loop through each property and determine if it must be added or changed   
            PropertyInfo[] properties = cache.GetAllProperties();

            foreach (PropertyInfo prop in properties)
            {
                propname = Formatter.GetPropertyName(prop, ".");

                //Find matching name from data definition
                index = definition.Names.IndexOf(propname);

                //If not found then add
                if (index == -1)
                {
                    modification.Added.Add(prop);
                }
                else
                {
                    bool flag = false;

                    //Detect if data type has changed, also check precision here
                    if (definition.Types[index].ToLower() != SqlFromType(prop.PropertyType).ToLower())
                    {
                        modification.Changed.Add(prop);
                        flag = true;
                    }
                    else if (definition.Nulls[index] != GetTypeNullability(prop.PropertyType))
                    {
                        modification.Changed.Add(prop);
                        flag = true;
                    }

                    //Add to unchanged if no changes
                    if (!flag) modification.Unchanged.Add(prop);
                }
            }

            return modification;
        }

        #endregion
    }
}
